/*
 * This file is part of the dSploit.
 *
 * Copyleft of Simone Margaritelli aka evilsocket <evilsocket@gmail.com>
 *
 * dSploit is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * dSploit is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with dSploit.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.csploit.android.plugins;

import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import androidx.core.content.ContextCompat;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import org.csploit.android.R;
import org.csploit.android.core.ChildManager;
import org.csploit.android.core.Logger;
import org.csploit.android.core.Plugin;
import org.csploit.android.core.System;
import org.csploit.android.gui.dialogs.ConfirmDialog;
import org.csploit.android.gui.dialogs.ConfirmDialog.ConfirmDialogListener;
import org.csploit.android.gui.dialogs.ErrorDialog;
import org.csploit.android.gui.dialogs.InputDialog;
import org.csploit.android.gui.dialogs.InputDialog.InputDialogListener;
import org.csploit.android.net.Network;
import org.csploit.android.net.Target;
import org.csploit.android.net.Target.Port;
import org.csploit.android.tools.NMap;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class PortScanner extends Plugin {
  private TextView mTextDoc = null;
  private EditText mTextParameters = null;
  private FloatingActionButton mScanFloatingActionButton = null;
  private ProgressBar mScanProgress = null;
  private boolean mRunning = false;
  private ArrayList<String> mPortList = new ArrayList<>();
  private ArrayAdapter<String> mListAdapter = null;
  private Receiver mScanReceiver = null;
  private String mCustomPorts = null;
  private Menu mMenu = null;
  private static final String CUSTOM_PARAMETERS = "PortScanner.Prefs.CustomParameters";
  private static final String CUSTOM_PARAMETERS_TEXT = "PortScanner.Prefs.CustomParameters.Text";
  private SharedPreferences mPreferences = null;
  private Map<Integer, String> urlFormats = new HashMap<>();
  private boolean mShowCustomParameters = false;

  public PortScanner() {
    super(R.string.port_scanner, R.string.port_scanner_desc,

            new Target.Type[]{Target.Type.ENDPOINT, Target.Type.REMOTE},
            R.layout.plugin_portscanner, R.drawable.action_scanner);
    urlFormats.put(80, "http://%s/");
    urlFormats.put(443, "https://%s/");
    urlFormats.put(21, "ftp://%s");
    urlFormats.put(22, "ssh://%s");
    urlFormats.put(23, "telnet://%s/");
    urlFormats.put(0, "telnet://%s:%d/"); ///< default
  }

  /**
   * Sets visible the custom parameters text field, and loads the saved parameters
   */
  private void displayParametersField() {
    mTextDoc.setVisibility(View.VISIBLE);
    mTextParameters.setVisibility(View.VISIBLE);
    mTextParameters.setText(mPreferences.getString(CUSTOM_PARAMETERS_TEXT, ""));

    mShowCustomParameters = true;
    saveCustomParameters();
  }

  /**
   * Hides the custom parameters text field, saving the typed parameters
   */
  private void hideParametersField() {
    mShowCustomParameters = false;
    saveCustomParameters();

    mTextDoc.setVisibility(View.GONE);
    mTextParameters.setVisibility(View.GONE);
  }

  /**
   * Saves customs parameters entered by the user
   */
  private void saveCustomParameters() {
    SharedPreferences.Editor edit = mPreferences.edit();
    edit.putBoolean(CUSTOM_PARAMETERS, mShowCustomParameters);
    edit.putString(CUSTOM_PARAMETERS_TEXT, mTextParameters.getText().toString());
    edit.commit();
  }

  private void setStoppedState() {
    if (mProcess != null) {
      mProcess.kill();
      mProcess = null;
    }
    saveCustomParameters();

    mScanProgress.setVisibility(View.INVISIBLE);
    mRunning = false;
    mScanFloatingActionButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_play_arrow_24dp));

    if (mPortList.size() == 0)
      Toast.makeText(this, getString(R.string.no_open_ports),
              Toast.LENGTH_LONG).show();
  }

  private void setStartedState() {
    createPortList();

    try {
      if (mShowCustomParameters) {
        mProcess = System.getTools().nmap
                .customScan(System.getCurrentTarget(), mScanReceiver, mTextParameters.getText().toString());
      } else {
        mProcess = System.getTools().nmap
                .synScan(System.getCurrentTarget(), mScanReceiver, mCustomPorts);
      }

      mRunning = true;
    } catch (ChildManager.ChildNotStartedException e) {
      System.errorLogging(e);
      Toast.makeText(PortScanner.this, getString(R.string.child_not_started) + "\n" + e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
    }
    mScanFloatingActionButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_stop_24dp));
  }

  private void createPortList() {
    mPortList.clear();

    for (Port p : System.getCurrentTarget().getOpenPorts()) {
      int pNumber = p.getNumber();
      String resolvedProtocol = System.getProtocolByPort(pNumber);
      String str;

      if (resolvedProtocol != null)
        str = pNumber + " ( " + resolvedProtocol + " )";
      else
        str = p.getProtocol().toString().toLowerCase() + " : " + pNumber;

      if (!mPortList.contains(str))
        mPortList.add(str);
    }

  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    SharedPreferences themePrefs = getSharedPreferences("THEME", 0);
    Boolean isDark = themePrefs.getBoolean("isDark", false);
    if (isDark)
      setTheme(R.style.DarkTheme);
    else
      setTheme(R.style.AppTheme);
    super.onCreate(savedInstanceState);

    mPreferences = System.getSettings();
    mTextDoc = (TextView) findViewById(R.id.scanDoc);
    mTextParameters = (EditText) findViewById(R.id.scanParameters);
    mScanFloatingActionButton = (FloatingActionButton) findViewById(R.id.scanToggleButton);
    mScanProgress = (ProgressBar) findViewById(R.id.scanActivity);

    mShowCustomParameters = mPreferences.getBoolean(CUSTOM_PARAMETERS, false);

    if (mShowCustomParameters)
      displayParametersField();
    else
      hideParametersField();

    mScanFloatingActionButton.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        if (mRunning) {
          setStoppedState();
        } else {
          setStartedState();
        }
      }
    });

    ListView mScanList = (ListView) findViewById(R.id.scanListView);

    createPortList();

    final Target target = System.getCurrentTarget();
    final String cmdlineRep = target.getCommandLineRepresentation();

    mScanReceiver = new Receiver(target);

    mListAdapter = new ArrayAdapter<>(this,
            android.R.layout.simple_list_item_1, mPortList);
    mScanList.setAdapter(mListAdapter);
    mScanList.setOnItemLongClickListener(new OnItemLongClickListener() {
      @Override
      public boolean onItemLongClick(AdapterView<?> parent, View view,
                                     int position, long id) {
        int portNumber = target.getOpenPorts().get(position).getNumber();

        if(!urlFormats.containsKey(portNumber)) {
          portNumber = 0;
        }

        final String url = String.format(urlFormats.get(portNumber),
                cmdlineRep, portNumber);

        new ConfirmDialog("Open", "Open " + url + " ?",
                PortScanner.this, new ConfirmDialogListener() {
          @Override
          public void onConfirm() {
            try {
              Intent browser = new Intent(
                      Intent.ACTION_VIEW, Uri.parse(url));

              PortScanner.this.startActivity(browser);
            } catch (ActivityNotFoundException e) {
              System.errorLogging(e);

              new ErrorDialog(
                      getString(R.string.error),
                      getString(R.string.no_activities_for_url),
                      PortScanner.this).show();
            }

          }

          @Override
          public void onCancel() {
          }
        }).show();

        return false;
      }
    });
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.port_scanner, menu);
    mMenu = menu;
    mMenu.findItem(R.id.scanner_custom_parameters).
            setChecked(mPreferences.getBoolean(CUSTOM_PARAMETERS, false));

    return super.onCreateOptionsMenu(menu);
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      case R.id.scanner_custom_parameters:
        if (item.isChecked())
          hideParametersField();
        else
          displayParametersField();

        item.setChecked(!item.isChecked());
        return true;
      case R.id.select_ports:

        new InputDialog(getString(R.string.select_ports),
                getString(R.string.enter_ports_list), this,
                new InputDialogListener() {
                  @Override
                  public void onInputEntered(String input) {
                    input = input.trim();

                    if (!input.isEmpty()) {
                      String[] ports = input.split("[^\\d]+");
                      for (String port : ports) {
                        try {
                          if (port.isEmpty())
                            throw new Exception(
                                    getString(R.string.invalid_port_)
                                            + port + "'.");

                          else {
                            int iport = Integer.parseInt(port);
                            if (iport <= 0 || iport > 65535)
                              throw new Exception(
                                      getString(R.string.port_must_be_greater));
                          }
                        } catch (Exception e) {
                          new ErrorDialog("Error", e.toString(),
                                  PortScanner.this).show();
                          return;
                        }
                      }

                      mCustomPorts = "";
                      for (int i = 0, last = ports.length - 1; i < ports.length; i++) {
                        mCustomPorts += ports[i];
                        if (i != last)
                          mCustomPorts += ",";
                      }

                      if (mCustomPorts.isEmpty()) {
                        mCustomPorts = null;
                        new ErrorDialog(getString(R.string.error),
                                getString(R.string.invalid_ports),
                                PortScanner.this).show();
                      }

                      hideParametersField();
                      mMenu.findItem(R.id.scanner_custom_parameters).setChecked(false);

                      Logger.debug("mCustomPorts = " + mCustomPorts);
                    } else
                      new ErrorDialog(getString(R.string.error),
                              getString(R.string.empty_port_list),
                              PortScanner.this).show();
                  }
                }).show();

        return true;

      default:
        return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public void onBackPressed() {
    setStoppedState();
    super.onBackPressed();
    overridePendingTransition(R.anim.fadeout, R.anim.fadein);
  }

  private class Receiver extends NMap.SynScanReceiver {

    private final Target target;

    public Receiver(Target target) {
      this.target = target;
    }

    @Override
    public void onStart(String commandLine) {
      super.onStart(commandLine);

      PortScanner.this.runOnUiThread(new Runnable() {
        @Override
        public void run() {
          mRunning = true;
          mScanProgress.setVisibility(View.VISIBLE);
        }
      });
    }

    @Override
    public void onEnd(int exitCode) {
      super.onEnd(exitCode);

      PortScanner.this.runOnUiThread(new Runnable() {
        @Override
        public void run() {
          setStoppedState();
        }
      });
    }

    @Override
    public void onPortFound(final int port, String protocol) {
      String resolvedProtocol = System.getProtocolByPort(port);

      target.addOpenPort(port, Network.Protocol.fromString(protocol));

      final String entry = (resolvedProtocol !=null ?
              port + " ( " + resolvedProtocol + " )" :
              protocol + " : " + port
      );

      if(!mPortList.contains(entry)) {
        PortScanner.this.runOnUiThread(new Runnable() {
          @Override
          public void run() {
            mPortList.add(entry);
            mListAdapter.notifyDataSetChanged();
          }
        });
      }
    }
  }
}
